﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Security;
using GoodStuff.Security;
using System.Web;
using System.Timers;

namespace GoodStuff.Web
{  
    /// <summary>
    /// Provides services for alternative Strongly-typed session state based on an encrypted cookie.
    /// Time-out is fixed by configuration. This class is to be used as a static member.
    /// </summary>
    public class StateServer<T> where T : class
    {
        private class StateServerEntry
        {
            public StateServerEntry()
            {
                Created = DateTime.Now;
            }

            public T state { get; set; }
            public DateTime Expires { get; set; }
            public DateTime Created { get; private set; }
        }

        private IDictionary<Guid, StateServerEntry> _cache = new Dictionary<Guid, StateServerEntry>();
        private Timer _cleanupTimer;

        private ICookieHelper _cookieHelper;

        public StateServer():this(new SecureCookieHelper())
        {
            // TODO register this stateserver with all the others.
        }

        public StateServer(ICookieHelper cookieHelper)
        {
            //TODO: Get from Configuration  -> timeout is for THIS cookie, it may be shared amonst other cookie names
            Timeout = TimeSpan.FromMinutes(15);
            CookieName = "SessionStateCookie";

            _cookieHelper = cookieHelper;
        }

        public TimeSpan Timeout { get; set; }
        public string CookieName { get; set; }

        public void Set(T state)
        {
            lock (this)
            {
                Guid key = GetOrCreateSessionKey();

                StateServerEntry myState;
                if (_cache.ContainsKey(key))
                {
                    myState = _cache[key];
                }
                else
                {
                    myState = new StateServerEntry();
                    _cache.Add(key, myState);
                }

                myState.Expires = DateTime.Now.Add(this.Timeout);
                myState.state = state;

                if (_cleanupTimer == null)
                {
                    //timer job to clean up stale data.
                    _cleanupTimer = new Timer(1000);    //every second.
                    _cleanupTimer.Elapsed += new ElapsedEventHandler(_cleanupTimer_Elapsed);
                    _cleanupTimer.Start();
                }
            }
        }

        void _cleanupTimer_Elapsed(object sender, ElapsedEventArgs e)
        {
            lock (this)
            {
                var expiredItems = _cache.Where(p => DateTime.Now > p.Value.Expires).ToList();
                foreach(var key in expiredItems)
                {
                    Cleanup(key.Value);
                    _cache.Remove(key.Key);
                }

                //list is empty. We can kill the timer.
                if (_cache.Count == 0)
                {
                    _cleanupTimer.Stop();
                    _cleanupTimer = null;
                }
            }            
        }

        /// <summary>
        /// Getting a state object also refreshes the timeout.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public T Get()
        {
            lock (this)
            {
                Guid? key = GetSessionKey();
                if (key != null)
                {
                    if (_cache.ContainsKey(key.Value))
                    {
                        StateServerEntry entry = _cache[key.Value];
                        if (DateTime.Now <= entry.Expires)
                        {
                            //update the expiry date.
                            entry.Expires = DateTime.Now.Add(this.Timeout);

                            return entry.state;
                        }

                        //State has expired. Clean me up.
                        Cleanup(entry);
                        _cache.Remove(key.Value);
                    }
                }
                return default(T);
            }
        }

        private void Cleanup(StateServerEntry entry)
        {
            if (entry != null && entry.state != null && entry.state is IDisposable)
            {
                ((IDisposable)entry.state).Dispose();
                entry.state = null;
            }
        }

        private Guid GetOrCreateSessionKey()
        {
            Guid? key = GetSessionKey();
            if (key == null)
            {
                key = Guid.NewGuid();

                _cookieHelper.SetCookieValue(CookieName, key.Value.ToString());
            }
            return key.Value;
        }

        
        private Guid? GetSessionKey()
        {
            string value = _cookieHelper.GetCookieValue(CookieName);
            if (!string.IsNullOrEmpty(value))
            {
                Guid key = new Guid(value);
                return key;
            }
            return null;
        }
    }    
}
